<?php
/**
 *  Yz_Action_Abstract implements the following methods of Yz_Action_Interface as final:
 *  - setDescriptor()
 *  - __invoke(), which is the alias for execute()
 */
abstract class Yz_Action_Abstract
    extends Yz_Object
    implements Yz_Action_Interface
{

    /**
     *  the list of action names inherited by this action
     *  @var array
     */
    protected $actionNames;

    /**
     *  the list of fields (field names are array values)
     *  @var array
     */
    protected $fields = array();

    /**
     *  the list of tables aka sources
     *  @var Yz_Hash
     */
    protected $tableList;

    /**
     *  the `default' table alias
     *  @var string
     */
    protected $primaryTable;

    /**
     *  @var Yz_SQL_Statement
     */
    protected $statement;


    /************************************
     *          Public methods          *
     ************************************/

    /**
     *  @param Yz_Hash $tableList
     *  @return ignored
     */
    public function __construct(Yz_Hash $tableList)
    {
        return $this->assignTables($tableList);
    }

    /** @see Yz_Action_Interface::assignFields()
     *  @param string|hash
     *  @return chainable
     */
    public function assignFields( $fields )
    {
        $this->fields = Yz_Array::valueOf($fields);
        return $this;
    }


    /**  __invoke(): call execute() and fail if no items affected
     *  @todo throw correct exception
     *  @todo invent correct name for method (like executeOrFail)
     *  @return mixed
     */
    public function __invoke()
    {
        if ($this->execute()->count() > 0) {
            return $this;
        }
        $ex = new \Yz_Exception_ItemNotFound();
        throw $ex;
    }


    /**
     *  get the number of affected items/records [fetched,updated,created,deleted]
     *  @return int
     */
    public function count()
    {
        return $this->getStatement()->count();
    }



    /************************************
     *        protected methods         *
     ************************************/


    /**
     *  try to get the associated statement, throw exception if it is not initialized
     *  @return SQL Statement
     *  @throws Yz_Exception
     */
    protected function getStatement()
    {
        if (null === $this->statement) {
            throw new Yz_Exception("Not executed yet");
        }
        return $this->statement;
    }


    /**
     *  determine all possible names of field `options' to be honored
     *  @return array
     */
    protected function getActionNames()
    {
        $names =& $this->actionNames;
        if (null === $names) {
            $names = array(lcfirst($this::shortClassName()));
            foreach ($this::listAncestors() as $class) {
                if (! is_subclass_of($class, 'Yz_Action_Interface')) {
                    break;
                }
                $name = lcfirst($class::shortClassName());
                if (! in_array($name, $names)) {
                    $names[] = $name;
                }
            }
        }
        return $names;
    }


    /**
     *
     *  @return array
     */
    protected function identifyFields(Yz_Hash $fields)
    {
        return $fields
            ->apply(array($this,'buildFieldClause'))
            ->filter()
            ->toArray()
            ;
    }


    /**
     *  An Insert will use this method only in case values were set
     *  An Update can not be performed without values
     *  @param hash
     *  @return chainable
     */
    protected function identifyValues($values)
    {
        if (0 === count($this->fields)) {
            throw new Yz_Exception("Fields must be assigned before assigning values");
        }
        if ($values instanceof ArrayAccess) {
            // OK
        } else {
            $values = new Yz_Hash($values);
        }
        $v = new Yz_Hash($this->fields);
        return $v->apply(function($index, $value) use ($values) {
            return $values->offsetExists($index)
                ? $values[$index]
                : null
                ;
        })
        ->toArray();
    }


    /** a callback for creating field clauses from fields array
     *  @param string $fieldName
     *  @param mixed $field (array|string)
     *  @return string the corresponding part of the field clause
     */
    public function buildFieldClause($fieldName, $field)
    {
        if ($field instanceof ArrayAccess) {
            $val = false;
            foreach ($this->getActionNames() as $name) {
                debug("Field {$fieldName}, action name {$name}");
                if ($field->offsetExists($name)) {
                    $val = $field->offsetGet($name);
                    debug("Value {$val}");
                    break;
                }
            }
        } else {
            $val = $field;
        }
        return $val;
    }

    /** assign source tables
     *  @param Yz_Hash $tableList
     *  @return chainable
     *  @throw Yz_Exception if table list empty
     */
    protected function assignTables(Yz_Hash $tableList)
    {
        if ($tableList->isEmpty()) {
            throw new Yz_Exception("No tables in source");
        }
        $this->tableList = $tableList;
        $this->primaryTable = $tableList->key();
        return $this;
    }

}
